15.3 Hibernate

现在要开始谈下 Spring 环境中的 Hibernate 3,用它来展示 Spring 对集成 O/R 映射的方法。本节将详细讨论许多问题,并显示不同的 DAO 实现和事务界定。大多数这些模式可以直接从所有其他支持 ORM 的工具中转换。以下部分在本章将覆盖其他的 ORM 技术,并显示简短的例子。

对于Spring 4.0, Spring 需要 Hibernate 3.6 或者更高版本

15.3.1 SessionFactory setup in a Spring container 在 Spring 容器中设置 SessionFactory

为了避免应用程序对象与硬编码的资源查找想绑定,您可以定义资源如 JDBC DataSource 或者 Hibernate SessionFactory 为 Spring 容器的 bean。应用对象需要通过对 bean 的引用来访问资源接受引用,就如在下一节中说明的 DAO 的定义。

以下摘录自 XML 应用程序上下文定义,展示了如何设置 JDBC DataSource 或者 Hibernate SessionFactory :

<beans>

    <bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
        <property name="url" value="jdbc:hsqldb:hsql://localhost:9001"/>
        <property name="username" value="sa"/>
        <property name="password" value=""/>
    </bean>

    <bean id="mySessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
        <property name="dataSource" ref="myDataSource"/>
        <property name="mappingResources">
            <list>
                <value>product.hbm.xml</value>
            </list>
        </property>
        <property name="hibernateProperties">
            <value>
                hibernate.dialect=org.hibernate.dialect.HSQLDialect
            </value>
        </property>
    </bean>

</beans>

从 Jakarta Commons DBCP BasicDataSource 转为 JNDI-located DataSource, 主要的配置为:

<beans>
    <jee:jndi-lookup id="myDataSource" jndi-name="java:comp/env/jdbc/myds"/>
</beans>

您还可以访问 JNDI-located SessionFactory,使用 Spring 的JndiObjectFactoryBean/<jee:jndi-lookup> 来检索和暴露他。然而,通常在 EJB 环境外不常用。

15.3.2 Implementing DAOs based on plain Hibernate 3 API

基于平常的 Hibernate 3 API 来实现 DAO

Hibernate 3 有一个特性称为上下文会话,Hibernate 本身在每个事务管理一个当前 Session 。这是大致相当于 Spring 的 每个事务一个当前 Hibernate Session的同步。相应的 DAO 实现像下面的例子,基于普通Hibernate API:

public class ProductDaoImpl implements ProductDao {

    private SessionFactory sessionFactory;

    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public Collection loadProductsByCategory(String category) {
        return this.sessionFactory.getCurrentSession()
                .createQuery("from test.Product product where product.category=?")
                .setParameter(0, category)
                .list();
    }
}

这种风格类似于 Hibernate 参考文档和例子,除了在一个实例变量中保存 SessionFactory。我们强烈建议基于实例的设置,替换老派的Hibernate 的 CaveatEmptor 示例应用程序中的static HibernateUtil类。(一般来说,不保留任何资源static变量,除非绝对必要)

上面的 DAO 是依赖注入模式:这正好符合 Spring IoC 容器,就像对Spring 的 HibernateTemplate 编码。当然,这种 DAO 也可以在普通的 Java 设置(例如,在单元测试)。简单的实例化,并用所需的工厂引用调用setSessionFactory(..)。Spring bean 定义 DAO 就像下面一样:

<beans>

    <bean id="myProductDao" class="product.ProductDaoImpl">
        <property name="sessionFactory" ref="mySessionFactory"/>
    </bean>

</beans>

DAO 风格的主要优点是,它只依赖于 Hibernate API ,而没有引进任何Spring 必需的类。从非侵入性的视角看这当然是有吸引力的,对Hibernate 开发者来说无疑会感觉更自然。

然而,DAO 将平常的 HibernateException(这是未检查的,所以不需要声明或者捕获),这意味着调用者当异常为一个普通的致命问题——除非他们想要依赖于 Hibernate 自身的异常结构。捕捉乐观锁定失败等具体原因是不可能除非把调用者与实现策略相联系。取消这交换是可接受的对于应用程序是基于 Hibernate 和/或 不需要任何特殊的异常处理。

幸运的是,Spring 的 LocalSessionFactoryBean 支持 Hibernate SessionFactory.getCurrentSession()方法用于任何 Spring 事务策略,返回当前 Spring 管理的事务 Session即使是HibernateTransactionManager.。当然,这种方法的标准行为返回仍然是当前Session与持续的JTA事务有关。这种行为适用于不管您使用是Spring 的 JtaTransactionManager,EJB容器管理的事务(CMT),或 JTA。

总之:你可以基于平常的 Hibernate 3 API 来实现 DAO,同时仍然能够参与 Spring 管理事务。

15.3.3 Declarative transaction demarcation 声明式事务划分

建议你使用 Spring 声明式事务的支持,这使您能够代替显式事务划分 API调用 AOP 事务拦截器中的 Java 代码。这个事务拦截器可以配置 Spring容器通过使用 Java 注释或 XML。这个声明式事务能力允许您保持业务服务中的重复性事务划分代码码更自由,并且让你只关注添加业务逻辑,而这是您的应用程序的真正价值。

在继续之前,强烈建议你读12.5节,“事务管理”如果你还没有这样做。

此外,事务语义比如传播行为和隔离水平可以在配置文件的改变,不影响业务服务的实现。

下面的示例说明如何使用 XML 配置 AOP 事务拦截器,一个简单的服务类:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- SessionFactory, DataSource, etc. omitted -->

    <bean id="transactionManager"
            class="org.springframework.orm.hibernate3.HibernateTransactionManager">
        <property name="sessionFactory" ref="sessionFactory"/>
    </bean>

    <aop:config>
        <aop:pointcut id="productServiceMethods"
                expression="execution(* product.ProductService.*(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="productServiceMethods"/>
    </aop:config>

    <tx:advice id="txAdvice" transaction-manager="myTxManager">
        <tx:attributes>
            <tx:method name="increasePrice*" propagation="REQUIRED"/>
            <tx:method name="someOtherBusinessMethod" propagation="REQUIRES_NEW"/>
            <tx:method name="*" propagation="SUPPORTS" read-only="true"/>
        </tx:attributes>
    </tx:advice>

    <bean id="myProductService" class="product.SimpleProductService">
        <property name="productDao" ref="myProductDao"/>
    </bean>

</beans>

下面是要处理的服务类

public class ProductServiceImpl implements ProductService {

    private ProductDao productDao;

    public void setProductDao(ProductDao productDao) {
        this.productDao = productDao;
    }

    // notice the absence of transaction demarcation code in this method
    // Spring's declarative transaction infrastructure will be demarcating
    // transactions on your behalf
    public void increasePriceOfAllProductsInCategory(final String category) {
        List productsToChange = this.productDao.loadProductsByCategory(category);
        // ...
    }
}

我们还展示了一个基于配置属性的支持,在下面的例子中。你通过 @Transactional注释的服务层,并引导 Spring 容器找到这些注释,这些注释的方法提供事务性语义。

public class ProductServiceImpl implements ProductService {

    private ProductDao productDao;

    public void setProductDao(ProductDao productDao) {
        this.productDao = productDao;
    }

    @Transactional
    public void increasePriceOfAllProductsInCategory(final String category) {
        List productsToChange = this.productDao.loadProductsByCategory(category);
        // ...
    }

    @Transactional(readOnly = true)
    public List<Product> findAllProducts() {
        return this.productDao.findAllProducts();
    }

}

正如你可以看到下面的配置实例,配置更加简化,与上述 XML 实例,同时还提供了在服务层的代码注释驱动相同的功能。所有您需要提供的是TransactionManager 的实现和"" 实体

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- SessionFactory, DataSource, etc. omitted -->

    <bean id="transactionManager"
            class="org.springframework.orm.hibernate3.HibernateTransactionManager">
        <property name="sessionFactory" ref="sessionFactory"/>
    </bean>

    <tx:annotation-driven/>

    <bean id="myProductService" class="product.SimpleProductService">
        <property name="productDao" ref="myProductDao"/>
    </bean>

</beans>

15.3.4 Programmatic transaction demarcation 编程式事务划分

可以在高级的应用中划分事务,在这样的底层数据访问服务生成任意数量的操作。限制也不存在于周边业务服务的实现;它只需要一个 Spring PlatformTransactionManager。再次,后者可以来自任何地方,但最好是通过 setTransactionManager(..)方法来对 bean 引用,正如由setProductDao(..)方法来设置 productDAO。下面的代码片段显示了在Spring 应用程序上下文中定义一个事务管理器和业务服务,以及一个业务方法实现:


    <beans>

        <bean id="myTxManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
            <property name="sessionFactory" ref="mySessionFactory"/>
        </bean>

        <bean id="myProductService" class="product.ProductServiceImpl">
            <property name="transactionManager" ref="myTxManager"/>
            <property name="productDao" ref="myProductDao"/>
        </bean>

    </beans>


    public class ProductServiceImpl implements ProductService {

        private TransactionTemplate transactionTemplate;
        private ProductDao productDao;

        public void setTransactionManager(PlatformTransactionManager transactionManager) {
            this.transactionTemplate = new TransactionTemplate(transactionManager);
        }

        public void setProductDao(ProductDao productDao) {
            this.productDao = productDao;
        }

        public void increasePriceOfAllProductsInCategory(final String category) {
            this.transactionTemplate.execute(new TransactionCallbackWithoutResult() {
                public void doInTransactionWithoutResult(TransactionStatus status) {
                    List productsToChange = this.productDao.loadProductsByCategory(category);
                    // do the price increase...
                }
            });
        }
    }

Spring TransactionInterceptor 允许任何检查的应用异常可以跟着回调代码抛出,而 TransactionTemplate是限制于未检查的回调的异常。当遇到一个未检查的应用异常或者是事务被应用(通过TransactionStatus)标记为 rollback-only(只回滚) ,TransactionTemplate触发回滚事务。默认时 TransactionInterceptor表现是一样的,但是允许在每个方法中配置回滚策略。

15.3.5 Transaction management strategies 事务管理策略

TransactionTemplateTransactionInterceptor代表了对 PlatformTransactionManager 实例的实际事务处理,在 Hibernate 的应用中它们可以是一个 HibernateTransactionManager(一个Hibernate 的 SessionFactory,在引擎下使用的是ThreadLocal Session)或 JtaTransactionManager(委派到容器的 JTA 子系统)。你甚至可以使用一个自定义的PlatformTransactionManager 实现。从原生 Hibernate 事务管理 转到 JTA,如你的某些应用程序部署具有分布式事务处理的要求,那么这仅仅是一个配置的问题,只要将 Hibernate 事务管理简单的替换为 Spring 的 JTA 即可。两个的事务划分和数据访问代码的不用改变,因为他们只是使用了通用的事务管理 API。

对于分布式事务跨多个 Hibernate 会话工厂,只要简单地把JtaTransactionManager 与具有多个定义的LocalSessionFactoryBean组合成为一个事务策略。每个 DAO 得到一个特定的SessionFactory的引用传递到其相应的 bean 属性。如果所有底层的 JDBC 数据源的事务容器,业务服务可以划分事务到任意数量的DAO 和任何数量的会话工厂,而这无需没有特殊的处理,只要是使用JtaTransactionManager策略。

<beans>

    <jee:jndi-lookup id="dataSource1" jndi-name="java:comp/env/jdbc/myds1"/>

    <jee:jndi-lookup id="dataSource2" jndi-name="java:comp/env/jdbc/myds2"/>

    <bean id="mySessionFactory1"
            class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
        <property name="dataSource" ref="myDataSource1"/>
        <property name="mappingResources">
            <list>
                <value>product.hbm.xml</value>
            </list>
        </property>
        <property name="hibernateProperties">
            <value>
                hibernate.dialect=org.hibernate.dialect.MySQLDialect
                hibernate.show_sql=true
            </value>
        </property>
    </bean>

    <bean id="mySessionFactory2"
            class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
        <property name="dataSource" ref="myDataSource2"/>
        <property name="mappingResources">
            <list>
                <value>inventory.hbm.xml</value>
            </list>
        </property>
        <property name="hibernateProperties">
            <value>
                hibernate.dialect=org.hibernate.dialect.OracleDialect
            </value>
        </property>
    </bean>

    <bean id="myTxManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>

    <bean id="myProductDao" class="product.ProductDaoImpl">
        <property name="sessionFactory" ref="mySessionFactory1"/>
    </bean>

    <bean id="myInventoryDao" class="product.InventoryDaoImpl">
        <property name="sessionFactory" ref="mySessionFactory2"/>
    </bean>

    <bean id="myProductService" class="product.ProductServiceImpl">
        <property name="productDao" ref="myProductDao"/>
        <property name="inventoryDao" ref="myInventoryDao"/>
    </bean>

    <aop:config>
        <aop:pointcut id="productServiceMethods"
                expression="execution(* product.ProductService.*(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="productServiceMethods"/>
    </aop:config>

    <tx:advice id="txAdvice" transaction-manager="myTxManager">
        <tx:attributes>
            <tx:method name="increasePrice*" propagation="REQUIRED"/>
            <tx:method name="someOtherBusinessMethod" propagation="REQUIRES_NEW"/>
            <tx:method name="*" propagation="SUPPORTS" read-only="true"/>
        </tx:attributes>
    </tx:advice>

</beans>

HibernateTransactionManagerJtaTransactionManager允许适当的 JVM 级别的 Hibernate 缓存处理,而不需要容器特定的事务管理查找或 JCA 连接器(如果你不使用 EJB 启动事务)。

HibernateTransactionManager可以为一个特定的DataSource导出 Hibernate JDBC Connection到普通 JDBC 访问代码。此功能允许高级的混合 Hibernate 和 JDBC 数据访问的完全没有 JTA 的事务划分,如果你只访问一个数据库。如果你有设置通过 LocalSessionFactoryBean类的dataSource属性传入SessionFactoryDataSourceHibernateTransactionManager自动暴露 Hibernate 事务作为一个 JDBC 事务。或者,您可以明确指定DataSource中哪些事务是需要支持通过 HibernateTransactionManager 类的dataSource属性暴露的。

15.3.6 Comparing container-managed and locally defined resources 比较容器管理和本地定义的资源

你可以在一个容器管理的 JNDI SessionFactory和本地定义的互相切换,而无需更改应用程序的代码。是否保持资源定义在容器或本地应用程序中,主要取决于使用的事务策略。对比 Spring 定义的本地的SessionFactory,手动注册 JNDI SessionFactory没有任何好处。部署一个通过 Hibernate JCA 连接器的SessionFactory来提供 Java EE 服务器的管理基础设施的附加值,但在这之前不增加任何实际的值。

Spring 的事务支持不是绑定到一个容器中。在配置除了 JTA 以外的任何策略后,事务支持同样也能在一个独立的或测试的环境中工作。特别是在单独的数据库事务的典型应用中。Spring 是一个轻量级的单资源本地事务支持和强大的 JTA 的替代品。当你使用本地 EJB 无状态会话 bean 来驱动事务,你都必须要依赖 EJB 容器和 JTA,即使你只访问一个数据库,并且只使用无状态会话 bean 通过容器管理的事务来提供声明式事务。另外,直接使用 JTA 编程需要一个 Java EE 环境。JTA 并不只涉及容器依赖性的JTA 本身和 JNDI DataSource 实例。对于 非 Spring,JTA 驱动的 Hibernate 事务交易,您必须使用 Hibernate JCA 连接器,或额外的Hibernate 事务代码TransactionManagerLookup为适当的 JVM 级别配置缓存。

Spring 驱动事务可以与本地定义的 Hibernate SessionFactory和本地的JDBC DataSource很好的工作,如果他们访问一个数据库。因此你只需要使用 Spring 的 JTA 事务策略,在当你有分布式事务的需求的时候。JCA 连接器需要特定容器部署步骤,显然首先需要的是 JCA 的支持。这个配置比部署一个简单的 使用本地资源定义和 Spring 驱动事务 web 应用程序需要更多的工作。同样,你经常需要你的容器是使用的是企业版,例如,WebLogic Express, 并且是不提供 JCA。Spring 的应用程序具有本地资源和事务跨越数据库的能力,可以在任何 Java EE web容器(没有 JTA、JCA 或 EJB )如Tomcat、Resin、甚至普通的 Jetty 中工作。此外,您可以很容易地重用这样的一个中间层在桌面应用程序或测试套件中。

从全面考虑,如果你不使用 EJB,请坚持使用本地 SessionFactory设置和 Spring 的HibernateTransactionManagerJtaTransactionManager。你得到所有的好处,包括适当的事务 JVM 级别缓存和分布式事务,没有容器部署的不便。JNDI 通过 JCA 连接器注册 Hibernate SessionFactory,在与 EJB 一起使用时只是增加了值。

15.3.7 Spurious application server warnings with Hibernate 在 Hibernate 中的虚假应用服务器告警

在一些 JTA 的非常严格的 XADataSource实现的环境中 — 目前只在一些 WebLogic Server 和 WebSphere 版本中 — 当 Hibernate 配置时没有留意环境中的 JTA 的 PlatformTransactionManager对象,这可能会导致 虚假告警或者异常显示在应用服务器的日志中。这些告警或者异常显示连接访问不再有效,或 JDBC 访问不再有效,这可能是因为事务已经不再活动了。举个例子,这是一个真实的 WebLogic 异常:

java.sql.SQLException: The transaction is no longer active - status: Committed. No
further JDBC access is allowed within this transaction.

要解决此警告,只需要使 Hibernate 知道 JTA PlatformTransactionManager实例,它将同步(连同 Spring)。实现这个有两个选项:

  • 如果在你的应用程序上下文中你已经直接获取 JTA PlatformTransactionManager对象(大概是从 JNDI 通过JndiObjectFactoryBean<jee:jndi-lookup>)将它提供给,例如,Spring 的 JtaTransactionManager,那么最简单的方法是通过引用定义了这个 JTA PlatformTransactionManager实例的 bean给LocalSessionFactoryBean指定一个jtaTransactionManager的属性值。那么 Spring 就会使对象在 Hibernate 中可用。
  • 你很有可能没有 JTA PlatformTransactionManager实例,因为Spring 的 JtaTransactionManager 本身可以找到它。因此,你需要配置 Hibernate 直接查找 JTA PlatformTransactionManager。你可以在 Hibernate 配置中通过配置应用程序服务器特定的ransactionManagerLookup类实现这个,正如 Hibernate 手册所描述的那样。

本节的其余部分描述了事件发生的顺序和 Hibernate 对 JTA PlatformTransactionManager的认知。

当 Hibernate 没有配置任何 JTA PlatformTransactionManager 的认知时,当一个 JTA 事务提交时以下事件发生:

  • JTA 事务提交。
  • Spring 的 JtaTransactionManager与 JTA 事务同步时,通过 JTA 事务管理执行一个 afterCompletion 的回调。
  • 在其他活动中,从 Spring 到 Hibernate 的同步可以触发回调,通过Hibernate 的 afterTransactionCompletion回调(用于清除 Hibernate 缓存),随后的是一个显式 close() 调用在 Hibernate Session,,导致 Hibernate 试图 close() JDBC 连接。
  • 在某些环境中,这个 Connection.close()调用然后触发警告或错误,因为应用程序服务器不再认为Connection是可用的,因为事务已经提交了。

当Hibernate 配置了 JTA PlatformTransactionManager的认知,当一个JTA事务提交,以下事件发生:

  • JTA 事务准备提交。
  • Spring 的 JtaTransactionManager 跟 JTA 事务是同步的,所以通过 JTA 事务管理器的 beforeCompletion回调来执行事务的回调。
  • Spring 感知到 Hibernate 本身与 JTA 事务是同步的,并且行为不同于在前面的场景。假设需要 Hibernate Session关闭, 那么 Spring将会关闭它。
  • JTA 事务提交。
  • Hibernate 与 JTA 事务是同步的,所以通过 JTA 事务管理器的 beforeCompletion回调来执行事务的回调,并能正确清楚其缓存。